Text Mining & NLP
Gran parte de los datos se encuentran no estructurados, es importante conocer técnicas que nos permitan obtener conclusiones a partir de los mensajes que generan nuestras organizaciones, clientes o usuarios.
Hoy aprenderemos algunas técnicas básicas para manipular cadenas de texto y aplicaremos técnicas de NLP a subtítulos para obtener algunas conclusiones.
Back2basics: Strings y expresiones regulares
Las cadenas o strings cumplen un papel importante en las tareas de ETL o preparación de los datos. Una de las librerías esenciales en la materia es string
Stringr es uno de los paquetes diseñados por Hadley Wickham para asistir en las tareas de manipulación de strings:
- se integra con pipes (%<%)
- sus funciones son consistentes y fáciles de interpretar
¿Qué son las cadenas de texto o strings?
- Las cadenas de texto se encuentran contenidas entre comillas
"" (*usar la comilla simple ' para escapar la doble comilla)
- Pueden contener letras
"a", números "1", simbolos "&" o todo lo anterior "1a&"
- Mientras que los números puede ser a la vez integers y characters, las letras y los s?mbolos no tienen significado como integer y se traducen en
NA
as.integer(c("a", "&", "123"))
[1] NA NA 123
c(factor("a"), "b", "&",1)
[1] "1" "b" "&" "1"
Concatenar integers y characters, convierte automáticamente los integers en characters.
c(as.character(factor("a")), "b", "&",1)
[1] "a" "b" "&" "1"
Operaciones básicas
Importar la librería
# install.packages("stringr")
library(stringr)
Operadores
Muchas de estas funciones tienne su equivalente en R base, pueden ser más lentas/menos eficientes
str_to_upper(string): convierte un string en mayúsculas
str_to_lower(string): convierte un string en minúsculas
str_to_title(string): capitaliza un string
temas <- c("Código", "Mujeres", "tecnología", "Informática", "estadística", "Women", "Coders", "Aprendizaje", "automático", "Análisis", "datos", "Visualización", "R-Ladies", "Social", "Coding", "R", "Ciencia", "Programming")
str_to_upper(temas)
[1] "CÓDIGO" "MUJERES" "TECNOLOGÍA" "INFORMÁTICA" "ESTADÍSTICA" "WOMEN" "CODERS"
[8] "APRENDIZAJE" "AUTOMÁTICO" "ANÁLISIS" "DATOS" "VISUALIZACIÓN" "R-LADIES" "SOCIAL"
[15] "CODING" "R" "CIENCIA" "PROGRAMMING"
str_to_lower(temas)
[1] "código" "mujeres" "tecnología" "informática" "estadística" "women" "coders"
[8] "aprendizaje" "automático" "análisis" "datos" "visualización" "r-ladies" "social"
[15] "coding" "r" "ciencia" "programming"
str_to_title(temas)
[1] "Código" "Mujeres" "Tecnología" "Informática" "Estadística" "Women" "Coders"
[8] "Aprendizaje" "Automático" "Análisis" "Datos" "Visualización" "R-Ladies" "Social"
[15] "Coding" "R" "Ciencia" "Programming"
str_c(string, sep = ""): junta varios string en uno solo, es el equivalente a paste(sep = "") o paste0()
str_length(string): devuelve la longitud del string, es similar a la función nchar(). Convierte los factores en strings y conserva los NA’s
print(str_length('R-Ladies'))
[1] 8
print(str_length(NA))
[1] NA
str_sub(string, start, end): subsetea un string o un vector de string especificando la posición inicial y la final, es el equivalente en R base a substr(). Por defecto finaliza en el último caracter.
print(temas[1:4])
[1] "Código" "Mujeres" "tecnología" "Informática"
str_sub(string = temas[1:4], start=3)
[1] "digo" "jeres" "cnología" "formática"
str_dup(string, times): copia y pega un string un número determinado de veces
str_dup(string = temas[1:4], times = 3)
[1] "CódigoCódigoCódigo" "MujeresMujeresMujeres" "tecnologíatecnologíatecnología"
[4] "InformáticaInformáticaInformática"
str_trim(string, side = c("both", "left", "rigth")): elimina los espacios vacíos, por defecto toma el valor both. Mejor evitar gsub(" ", "", string)
str_pad(string, width, side = c("left", "both", "right"), pad = " ")): añade a strings espacios en blanco para igualarlos en longitud, especialmente útil para añadir 0 a números.
Expresiones regulares
Las expresiones regulares ( regular expressions, regex, pattern matching) son un lenguaje usado para parsear y manipular texto. Se usan comúnmente para hacer operaciones de búsqueda y reemplazo y para validar si un texto está bien formado.
Expresiones comunes
- “a” = letra “a”
- “^a” = empieza con la letra “a”
- “a$” = finaliza con la letra “a”
- “[ ]” = contiene cualquier letra (o número) de las contenidas en los corchetes
- “[ - ]” = contiene cualquier letra (o número) dentro de un rango
- “[^ae]” = cualquier cosa excepto determinadas letras (o números)
- “{3}” = repite la expresión regular 3 veces
Las expresiones regulares son un mundo en si mismo, aquí tienes una pequeña chuleta : https://www.rstudio.com/wp-content/uploads/2016/09/RegExCheatsheet.pdf
rcosas = c("baseR", "R-Ladies", "Rmeetup", "Rmarkdown", "stringR")
str_detect(rcosas, pattern = "^R")
[1] FALSE TRUE TRUE TRUE FALSE
rcosas[str_detect(rcosas, pattern = "^R")]
[1] "R-Ladies" "Rmeetup" "Rmarkdown"
rcosas[str_detect(rcosas, pattern = "R")]
[1] "baseR" "R-Ladies" "Rmeetup" "Rmarkdown" "stringR"
cleanR <- c("tidyverse", "tidyr","dplyr", "ggplot2", "tidytext", "purrr")
str_locate(cleanR, "tidy")
start end
[1,] 1 4
[2,] 1 4
[3,] NA NA
[4,] NA NA
[5,] 1 4
[6,] NA NA
Otras funciones
str_extract(string, pattern) o str_extract_all(): busca la palabra exacta (normalmente se utiliza con expresiones regulares concatenadas)
str_match(string, pattern) o str_match_all(): es una función equivalente pero devuelve una matriz
str_match(c("12345678", "12587465", "dni desconocido"), pattern = "[1-9]{8}")
[,1]
[1,] "12345678"
[2,] "12587465"
[3,] NA
str_match_all(c("12345678", "12587465", "dni desconocido"), pattern = "[1-9]{8}")
[[1]]
[,1]
[1,] "12345678"
[[2]]
[,1]
[1,] "12587465"
[[3]]
[,1]
str_replace(string, pattern, replacement): reemplaza la primera instancia, str_replace_all para reemplazarlas todas
str_replace(c("castanya", "otonyo", "veronyo", "anyo", "nyonyo"), pattern = "ny", replacement = "ñ")
[1] "castaña" "otoño" "veroño" "año" "ñonyo"
str_replace_all(c("castanya", "otonyo", "veronyo", "anyo", "nyonyo"), pattern = "ny", replacement = "ñ")
[1] "castaña" "otoño" "veroño" "año" "ñoño"
str_split(string, pattern): separa una cadena en un vector, str_split_fixed(string, pattern, n) lo hace en un número n determinado de elementos
print(str_split("Eres muy chu chu chuli",pattern = " "))
[[1]]
[1] "Eres" "muy" "chu" "chu" "chuli"
print(length(str_split("Eres muy chu chu chuli",pattern = " ")[[1]]))
[1] 5
Analizando texto con R: los Simpsons fuente de sabiduría
Leer los subtitutos
La librería subtools permite leer archivos .str y .sub, así como organizar cada diálogo en un data frame. Los archivos han de estar organizados en directorios por temporadas y cada uno ha de estar nombrado como S01xE01 para que se parsee correctamente el número de temporada y de episodio.
Nos centraremos en las 9 primeras temporadas de los Simpsons. Si descargamos la puntuación y representamos gráficamente el promedio del score por temporada, se observa un claro descenso a partir de la 10. Por otro lado son las temporadas que mejor conocemos gracias a sus numerosas repeticiones :)
knitr::include_graphics("C:/Users/ClaudiaG/Documents/rladies/rladies_repo/rladies_textmining/images/ratings.png",dpi = 100)

A continuación leemos los subtítulos ( en inglés ) de las 9 primeras temporadas de los Simpsons.
library(subtools)
a <- read.subtitles.serie(dir = "C:/Users/ClaudiaG/Documents/rladies/rladies_repo/rladies_textmining/The Simpsons/")
Read: 9 seasons, 205 episodes
df <- subDataFrame(a)
df <- df[complete.cases(df), ]
str(df)
'data.frame': 58066 obs. of 8 variables:
$ ID : chr "<U+FEFF>1" "2" "3" "4" ...
$ Timecode.in : chr "00:00:00.042" "00:00:08.759" "00:00:10.761" "00:00:15.557" ...
$ Timecode.out: chr "00:00:00.042" "00:00:10.677" "00:00:13.180" "00:00:17.851" ...
$ Text : chr "23.976" "Ooh! Careful, Homer!" "There's no time. We're late." "O little town of Bethlehem" ...
$ season : chr "season 1" "season 1" "season 1" "season 1" ...
$ season_num : num 1 1 1 1 1 1 1 1 1 1 ...
$ episode_num : num 1 1 1 1 1 1 1 1 1 1 ...
$ serie : chr "The Simpsons" "The Simpsons" "The Simpsons" "The Simpsons" ...
Primer análisis: tm
Una primera opción es emplear la librería tm para nuestro análisis. La función tm_map nos va a permitir preparar nuestor documento para el análisis:
- conformar un corpus (conjunto estructurado de textos / documentos )
- transformar en minúsculas nuestro texto
- eliminar los signos de puntuación
- eliminar los números
- eliminar las stopwords
- eliminar los espacios en blanco
library(tm)
c <- tmCorpus(a)
c <- tm_map(c, content_transformer(tolower))
c <- tm_map(c, removePunctuation)
c <- tm_map(c, removeNumbers)
c <- tm_map(c, removeWords, stopwords("english"))
c <- tm_map(c, stripWhitespace)
c
<<SimpleCorpus>>
Metadata: corpus specific: 1, document level (indexed): 4
Content: documents: 205
El segundo paso, una vez preparado nuestro corpus, será el de construir la matriz de términos documentos. Para simplificar el análisis cada temporada constituirá un documento. De esta forma obtenemos para cada término la frecuencia por temporada.
TDM <- TermDocumentMatrix(c)
TDM <- as.matrix(TDM)
vec.season <- c(rep(x = 1,13), rep(2, 22), rep(3,24), rep(4,22), rep(5,24), rep(6,25), rep(7,25),rep(8,25), rep(9,25)) #episodios por temp
TDM.season <- t(apply(TDM, 1, function(x) tapply(x, vec.season, sum)))
colnames(TDM.season) <- paste0("S_", unique(vec.season))
head(TDM.season)
Terms S_1 S_2 S_3 S_4 S_5 S_6 S_7 S_8 S_9
able 6 4 3 0 6 7 4 2 4
aboard 1 1 2 2 2 2 2 1 1
acts 1 1 1 0 0 1 0 0 1
adult 1 3 2 0 3 3 1 1 1
affecting 1 0 0 0 0 0 0 0 0
afford 5 8 5 1 2 4 8 6 1
A continuación representamos en una nube de términos dichas frecuencias: el tamaño indica el número de repeticiones y el color la temporada en la que más repeticiones presenta. La posición del término respecto de la etiqueta de temporada indica también la frecuencia por temporada.

Text mining con tidytext
La librería tidytext nos permite realizar operaciones como tf (frecuencia de términos) o tf_idf (frecuencia de término - frecuencia inversa de documento) con mayor agilidad.
De nuevo vamos a preparar el nuestros diálogos para el análisis eliminado las stopwords.
library(tidytext)
library(tidyverse)
data(stop_words)
tidy_df <- df %>%
unnest_tokens(word, Text) %>%
dplyr::anti_join(stop_words)
Joining, by = "word"
Eliminamos además aquellas “palabras” constituidas exclusivamente por números y la palabra simpson.
library(data.table)
tidy_df <- as.data.table(tidy_df)
tidy_df <- tidy_df[is.na(as.numeric(word))]
NAs introduced by coercion
tidy_df <- tidy_df[word != 'simpson']
Con nuestro data set limpio podemos representar en un gráfico de barras la frecuencia de términos. Lo que haremos será:
- agrupar por temporada
- sumar el número de veces que aparece el término (
count)
- tomar el top 10 por temporada
- representar en un barplot para cada temporada dicha frecuencia
library(ggplot2)
tidy_df %>% group_by(season) %>%
count(word, sort = TRUE) %>%
top_n(15) %>%
ggplot(aes(reorder(word,n), n, fill = season)) +
geom_col() +
coord_flip() +
facet_wrap(~season, scales = "free_y") +
labs(x = NULL) +
guides(fill = FALSE) +
scale_fill_brewer(palette = "Set1")
Selecting by n

Ahora que disponemos de la frecuencia podemos analizar la evolución en sus apariciones / tramas de cada personaje: + calculamos la frecuencia para todos los términos, no solo el top 15 + creamos dos listas, la familia Simpson y otros personajes relevantes de la trama + representamos ambas series series temporales
También podemos obtener y representar los bigramas (conjunto de 2 términos) y sus frecuencias. Se representan en forma de grafo los más comunes (aquellos que aparecen al menos 7 veces en una misma temporada)


También podemos realizar tf_idf sobre nuestro corpus para localizar las palabras más relevantes, en general y por temporada
tf_idf_df %>%
top_n(20) %>%
ggplot(aes(word, tf_idf, fill = season)) +
geom_col() +
labs(x = NULL, y = "tf-idf") +
coord_flip()
Selecting by tf_idf

tf_idf_df %>%
group_by(season) %>%
top_n(8) %>%
ungroup %>%
ggplot(aes(reorder(word, tf_idf), tf_idf, fill = season)) +
geom_col(show.legend = FALSE) +
labs(x = NULL, y = "tf-idf") +
facet_wrap(~season, ncol = 2, scales = "free") +
coord_flip()
Selecting by tf_idf

LS0tDQp0aXRsZTogIlItTGFkaWVzICsgTGluZ3dhcnM6IFRhbGxlciBkZSBtaW5lcu1hIGRlIHRleHRvcyBjb24gUiINCnN1YnRpdGxlOiAiUi1MYWRpZXMgTWFkcmlkLCBWZXLzbmljYSBHYXJj7WEgeSBDbGF1ZGlhIEd1aXJhbywgMTcvMTAvMjAxNyINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGZpZ19jYXB0aW9uOiB5ZXMNCiAgICBmaWdfd2lkdGg6IDgNCiAgICB0b2M6IHllcw0KLS0tDQojIyC/UXVp6W5lcyBzb21vcz8NCg0KLSAqKlZlcvNuaWNhIEdhcmPtYSoqLCBAeXJ5YWEgDQotICoqQ2xhdWRpYSBHdWlyYW8qKiwgQGNsYXVkaWFndWlyYW8gDQoNClJMYWRpZXMgeSBEYXRhIFNjaWVudGlzdHMgZW4gKipLZXJuZWwgQW5hbHl0aWNzKiogDQoNCkPzZGlnbyB5IGRhdG9zIGVuIGh0dHBzOi8vZ2l0aHViLmNvbS9pbnRpdmVkYS9ybGFkaWVzX3RleHRtaW5pbmcgDQoNCiMjIFRleHQgTWluaW5nICYgTkxQDQoNCkdyYW4gcGFydGUgZGUgbG9zIGRhdG9zIHNlIGVuY3VlbnRyYW4gbm8gZXN0cnVjdHVyYWRvcywgZXMgaW1wb3J0YW50ZSBjb25vY2VyIHTpY25pY2FzIHF1ZSBub3MgcGVybWl0YW4gb2J0ZW5lciBjb25jbHVzaW9uZXMgYSBwYXJ0aXIgZGUgbG9zIG1lbnNhamVzIHF1ZSBnZW5lcmFuIG51ZXN0cmFzIG9yZ2FuaXphY2lvbmVzLCBjbGllbnRlcyBvIHVzdWFyaW9zLiANCg0KSG95IGFwcmVuZGVyZW1vcyBhbGd1bmFzIHTpY25pY2FzIGLhc2ljYXMgcGFyYSBtYW5pcHVsYXIgY2FkZW5hcyBkZSB0ZXh0byB5IGFwbGljYXJlbW9zIHTpY25pY2FzIGRlIE5MUCBhIHN1YnTtdHVsb3MgcGFyYSBvYnRlbmVyIGFsZ3VuYXMgY29uY2x1c2lvbmVzLiANCg0KIyMgTWF0ZXJpYWxlcyB5IGxpYnJlcu1hcyANCg0KKiBTdWJ07XR1bG9zIHRlbXBvcmFkYXMgZGUgbG9zIFNpbXBzb25zDQoqIExpYnJlcu1hczoNCiArIGBgYHN0cmluZ3JgYGANCiArIGBgYHN1YnRvb2xzYGBgDQogKyBgYGB0bWBgYA0KICsgYGBgdGlkeXRleHRgYGANCiArIGBgYHRpZHl2ZXJzZWBgYCANCiArIGBgYGRwbHlyYGBgDQogKyBgYGBkYXRhdGFibGVgYGANCiArIGBgYGdncGxvdDJgYGANCiArIGBgYHBsb3RseWBgYChvcGNpb25hbCkNCiArIGBgYGlncmFwaGBgYA0KICsgYGBgZ2dyYXBoYGBgDQogKyBgYGB3b3JsZGNsb3VkYGBgDQogKyBgYGBrbml0YGBgDQoNCiMjIEJhY2syYmFzaWNzOiBTdHJpbmdzIHkgZXhwcmVzaW9uZXMgcmVndWxhcmVzDQoNCkxhcyBjYWRlbmFzIG8gc3RyaW5ncyBjdW1wbGVuIHVuIHBhcGVsIGltcG9ydGFudGUgZW4gbGFzIHRhcmVhcyBkZSBFVEwgbyBwcmVwYXJhY2nzbiBkZSBsb3MgZGF0b3MuIFVuYSBkZSBsYXMgbGlicmVy7WFzIGVzZW5jaWFsZXMgZW4gbGEgbWF0ZXJpYSBlcyAqKnN0cmluZyoqDQoNCmBgYFN0cmluZ3JgYGAgZXMgdW5vIGRlIGxvcyBwYXF1ZXRlcyBkaXNl8WFkb3MgcG9yIEhhZGxleSBXaWNraGFtIHBhcmEgYXNpc3RpciBlbiBsYXMgdGFyZWFzIGRlIG1hbmlwdWxhY2nzbiBkZSBzdHJpbmdzOg0KDQorIHNlIGludGVncmEgY29uIF9waXBlc18gKCU8JSkNCisgc3VzIGZ1bmNpb25lcyBzb24gY29uc2lzdGVudGVzIHkgZuFjaWxlcyBkZSBpbnRlcnByZXRhcg0KDQojIyMgv1F16SBzb24gbGFzIGNhZGVuYXMgZGUgdGV4dG8gbyBfc3RyaW5nc18/DQoNCisgTGFzIGNhZGVuYXMgZGUgdGV4dG8gc2UgZW5jdWVudHJhbiBjb250ZW5pZGFzIGVudHJlIGNvbWlsbGFzIGAiImAgKCp1c2FyIGxhIGNvbWlsbGEgc2ltcGxlIGAnYCBwYXJhIGVzY2FwYXIgbGEgZG9ibGUgY29taWxsYSkNCisgUHVlZGVuIGNvbnRlbmVyIGxldHJhcyBgImEiYCwgbvptZXJvcyBgIjEiYCwgc2ltYm9sb3MgYCImImAgbyB0b2RvIGxvIGFudGVyaW9yIGAiMWEmImANCisgTWllbnRyYXMgcXVlIGxvcyBu+m1lcm9zIHB1ZWRlIHNlciBhIGxhIHZleiBfaW50ZWdlcnNfIHkgX2NoYXJhY3RlcnNfLCBsYXMgbGV0cmFzIHkgbG9zIHM/bWJvbG9zIG5vIHRpZW5lbiBzaWduaWZpY2FkbyBjb21vIGludGVnZXIgeSBzZSB0cmFkdWNlbiBlbiBgTkFgDQoNCmBgYHtyIGFzLmludGVnZXIsIHdhcm5pbmc9RkFMU0V9DQphcy5pbnRlZ2VyKGMoImEiLCAiJiIsICIxMjMiKSkNCmBgYA0KDQpgYGB7ciBsaXN0MX0NCmMoZmFjdG9yKCJhIiksICJiIiwgIiYiLDEpDQpgYGANCkNvbmNhdGVuYXIgaW50ZWdlcnMgeSBjaGFyYWN0ZXJzLCBjb252aWVydGUgYXV0b23hdGljYW1lbnRlIGxvcyBpbnRlZ2VycyBlbiBfY2hhcmFjdGVyc18uIA0KDQpgYGB7ciBsaXN0Mn0NCmMoYXMuY2hhcmFjdGVyKGZhY3RvcigiYSIpKSwgImIiLCAiJiIsMSkNCmBgYA0KDQojIyMgT3BlcmFjaW9uZXMgYuFzaWNhcw0KDQojIyMjIEltcG9ydGFyIGxhIGxpYnJlcu1hDQpgYGB7cn0NCiMgaW5zdGFsbC5wYWNrYWdlcygic3RyaW5nciIpDQpsaWJyYXJ5KHN0cmluZ3IpDQpgYGANCiMjIyMgT3BlcmFkb3Jlcw0KDQpNdWNoYXMgZGUgZXN0YXMgZnVuY2lvbmVzIHRpZW5uZSBzdSBlcXVpdmFsZW50ZSBlbiBSIGJhc2UsIHB1ZWRlbiBzZXIgbeFzIGxlbnRhcy9tZW5vcyBlZmljaWVudGVzDQoNCisgYHN0cl90b191cHBlcihzdHJpbmcpYDogY29udmllcnRlIHVuIHN0cmluZyBlbiBtYXn6c2N1bGFzDQorIGBzdHJfdG9fbG93ZXIoc3RyaW5nKWA6IGNvbnZpZXJ0ZSB1biBzdHJpbmcgZW4gbWlu+nNjdWxhcw0KKyBgc3RyX3RvX3RpdGxlKHN0cmluZylgOiBjYXBpdGFsaXphIHVuIHN0cmluZw0KDQoNCmBgYHtyIHRlbWFzfQ0KdGVtYXMgPC0gYygiQ/NkaWdvIiwgIk11amVyZXMiLCAidGVjbm9sb2ftYSIsICJJbmZvcm3hdGljYSIsICJlc3RhZO1zdGljYSIsICJXb21lbiIsICJDb2RlcnMiLCAiQXByZW5kaXphamUiLCAiYXV0b23hdGljbyIsICJBbuFsaXNpcyIsICJkYXRvcyIsICJWaXN1YWxpemFjafNuIiwgIlItTGFkaWVzIiwgIlNvY2lhbCIsICJDb2RpbmciLCAiUiIsICJDaWVuY2lhIiwgIlByb2dyYW1taW5nIikNCmBgYA0KYGBge3IgbWF5dXN9DQpzdHJfdG9fdXBwZXIodGVtYXMpDQpgYGANCmBgYHtyIG1pbnVzfQ0Kc3RyX3RvX2xvd2VyKHRlbWFzKQ0KYGBgDQpgYGB7ciBjYXBpfQ0Kc3RyX3RvX3RpdGxlKHRlbWFzKQ0KYGBgDQoNCisgYHN0cl9jKHN0cmluZywgc2VwID0gIiIpYDoganVudGEgdmFyaW9zIHN0cmluZyBlbiB1bm8gc29sbywgZXMgZWwgZXF1aXZhbGVudGUgYSBgcGFzdGUoc2VwID0gIiIpYCBvIGBwYXN0ZTAoKWANCisgYHN0cl9sZW5ndGgoc3RyaW5nKWA6IGRldnVlbHZlIGxhIGxvbmdpdHVkIGRlbCBzdHJpbmcsIGVzIHNpbWlsYXIgYSBsYSBmdW5jafNuIGBuY2hhcigpYC4gQ29udmllcnRlIGxvcyBmYWN0b3JlcyBlbiBzdHJpbmdzIHkgY29uc2VydmEgbG9zIE5BJ3MNCg0KYGBge3IgbGVuZ3RofQ0KcHJpbnQoc3RyX2xlbmd0aCgnUi1MYWRpZXMnKSkNCnByaW50KHN0cl9sZW5ndGgoTkEpKQ0KYGBgDQoNCisgYHN0cl9zdWIoc3RyaW5nLCBzdGFydCwgZW5kKWA6IHN1YnNldGVhIHVuIHN0cmluZyBvIHVuIHZlY3RvciBkZSBzdHJpbmcgZXNwZWNpZmljYW5kbyBsYSBwb3NpY2nzbiBpbmljaWFsIHkgbGEgZmluYWwsIGVzIGVsIGVxdWl2YWxlbnRlIGVuIFIgYmFzZSBhIHN1YnN0cigpLiBQb3IgZGVmZWN0byBmaW5hbGl6YSBlbiBlbCD6bHRpbW8gY2FyYWN0ZXIuDQoNCmBgYHtyIHN1YnNldHRpbmcyfQ0KcHJpbnQodGVtYXNbMTo0XSkNCnN0cl9zdWIoc3RyaW5nID0gdGVtYXNbMTo0XSwgc3RhcnQ9MykNCmBgYA0KKyBgc3RyX2R1cChzdHJpbmcsIHRpbWVzKWA6IGNvcGlhIHkgcGVnYSB1biBzdHJpbmcgdW4gbvptZXJvIGRldGVybWluYWRvIGRlIHZlY2VzDQpgYGB7ciBkdXBsaWNhdGluZ30NCnN0cl9kdXAoc3RyaW5nID0gdGVtYXNbMTo0XSwgdGltZXMgPSAzKQ0KYGBgDQoNCisgYHN0cl90cmltKHN0cmluZywgc2lkZSA9IGMoImJvdGgiLCAibGVmdCIsICJyaWd0aCIpKWA6IGVsaW1pbmEgbG9zIGVzcGFjaW9zIHZhY+1vcywgcG9yIGRlZmVjdG8gdG9tYSBlbCB2YWxvciBfYm90aF8uIE1lam9yIGV2aXRhciBgZ3N1YigiICIsICIiLCBzdHJpbmcpYCANCg0KKyBgc3RyX3BhZChzdHJpbmcsIHdpZHRoLCBzaWRlID0gYygibGVmdCIsICJib3RoIiwgInJpZ2h0IiksIHBhZCA9ICIgIikpYDogYfFhZGUgYSBzdHJpbmdzIGVzcGFjaW9zIGVuIGJsYW5jbyBwYXJhIGlndWFsYXJsb3MgZW4gbG9uZ2l0dWQsIGVzcGVjaWFsbWVudGUg+nRpbCBwYXJhIGHxYWRpciAwIGEgbvptZXJvcy4gDQoNCiMjIyBFeHByZXNpb25lcyByZWd1bGFyZXMNCg0KTGFzIGV4cHJlc2lvbmVzIHJlZ3VsYXJlcyAoIF9yZWd1bGFyIGV4cHJlc3Npb25zXywgX3JlZ2V4XywgX3BhdHRlcm4gbWF0Y2hpbmdfKSBzb24gdW4gbGVuZ3VhamUgdXNhZG8gcGFyYSBwYXJzZWFyIHkgbWFuaXB1bGFyIHRleHRvLiBTZSB1c2FuIGNvbfpubWVudGUgcGFyYSBoYWNlciBvcGVyYWNpb25lcyBkZSBi+nNxdWVkYSB5IHJlZW1wbGF6byB5IHBhcmEgdmFsaWRhciBzaSB1biB0ZXh0byBlc3ThIGJpZW4gZm9ybWFkby4gDQoNCiMjIyMjIEV4cHJlc2lvbmVzIGNvbXVuZXMNCg0KLSAiYSIgID0gbGV0cmEgImEiDQotICJeYSIgPSBlbXBpZXphIGNvbiBsYSBsZXRyYSAiYSINCi0gImEkIiA9IGZpbmFsaXphIGNvbiBsYSBsZXRyYSAiYSINCi0gIlsgXSIgPSBjb250aWVuZSBjdWFscXVpZXIgbGV0cmEgKG8gbvptZXJvKSBkZSBsYXMgY29udGVuaWRhcyBlbiBsb3MgY29yY2hldGVzDQotICJbIC0gXSIgPSBjb250aWVuZSBjdWFscXVpZXIgbGV0cmEgKG8gbvptZXJvKSBkZW50cm8gZGUgdW4gcmFuZ28NCi0gIlteYWVdIiA9IGN1YWxxdWllciBjb3NhIGV4Y2VwdG8gZGV0ZXJtaW5hZGFzIGxldHJhcyAobyBu+m1lcm9zKQ0KLSAiezN9IiA9IHJlcGl0ZSBsYSBleHByZXNp824gcmVndWxhciAzIHZlY2VzDQoNCkxhcyBleHByZXNpb25lcyByZWd1bGFyZXMgc29uIHVuIG11bmRvIGVuIHNpIG1pc21vLCBhcXXtIHRpZW5lcyB1bmEgcGVxdWXxYSBfY2h1bGV0YV8gOiBodHRwczovL3d3dy5yc3R1ZGlvLmNvbS93cC1jb250ZW50L3VwbG9hZHMvMjAxNi8wOS9SZWdFeENoZWF0c2hlZXQucGRmIA0KDQoNCmBgYHtyIHJlZ2V4fQ0KcmNvc2FzID0gYygiYmFzZVIiLCAiUi1MYWRpZXMiLCAiUm1lZXR1cCIsICJSbWFya2Rvd24iLCAic3RyaW5nUiIpDQpzdHJfZGV0ZWN0KHJjb3NhcywgcGF0dGVybiA9ICJeUiIpDQpgYGANCmBgYHtyIHJlZ2V4Mn0NCnJjb3Nhc1tzdHJfZGV0ZWN0KHJjb3NhcywgcGF0dGVybiA9ICJeUiIpXQ0KYGBgDQoNCmBgYHtyIHJlZ2V4M30NCnJjb3Nhc1tzdHJfZGV0ZWN0KHJjb3NhcywgcGF0dGVybiA9ICJSIildDQpgYGANCmBgYHtyIHJlZ2V4NH0NCmNsZWFuUiA8LSBjKCJ0aWR5dmVyc2UiLCAidGlkeXIiLCJkcGx5ciIsICJnZ3Bsb3QyIiwgInRpZHl0ZXh0IiwgInB1cnJyIikNCnN0cl9sb2NhdGUoY2xlYW5SLCAidGlkeSIpDQpgYGANCg0KIyMjIyMgT3RyYXMgZnVuY2lvbmVzIA0KKyBgc3RyX2V4dHJhY3Qoc3RyaW5nLCBwYXR0ZXJuKWAgbyBgc3RyX2V4dHJhY3RfYWxsKClgOiBidXNjYSBsYSBwYWxhYnJhIGV4YWN0YSAobm9ybWFsbWVudGUgc2UgdXRpbGl6YSBjb24gZXhwcmVzaW9uZXMgcmVndWxhcmVzIGNvbmNhdGVuYWRhcykNCisgYHN0cl9tYXRjaChzdHJpbmcsIHBhdHRlcm4pYCBvIGBzdHJfbWF0Y2hfYWxsKClgOiBlcyB1bmEgZnVuY2nzbiBlcXVpdmFsZW50ZSBwZXJvIGRldnVlbHZlIHVuYSBtYXRyaXoNCg0KYGBge3IgcmVnZXg1fQ0Kc3RyX21hdGNoKGMoIjEyMzQ1Njc4IiwgIjEyNTg3NDY1IiwgImRuaSBkZXNjb25vY2lkbyIpLCBwYXR0ZXJuID0gIlsxLTldezh9IikNCmBgYA0KYGBge3IgcmVnZXg2fQ0Kc3RyX21hdGNoX2FsbChjKCIxMjM0NTY3OCIsICIxMjU4NzQ2NSIsICJkbmkgZGVzY29ub2NpZG8iKSwgcGF0dGVybiA9ICJbMS05XXs4fSIpDQpgYGANCg0KDQorIGBzdHJfcmVwbGFjZShzdHJpbmcsIHBhdHRlcm4sIHJlcGxhY2VtZW50KWA6IHJlZW1wbGF6YSBsYSBwcmltZXJhIGluc3RhbmNpYSwgYHN0cl9yZXBsYWNlX2FsbGAgcGFyYSByZWVtcGxhemFybGFzIHRvZGFzDQpgYGB7ciByZWdleDd9DQpzdHJfcmVwbGFjZShjKCJjYXN0YW55YSIsICJvdG9ueW8iLCAidmVyb255byIsICJhbnlvIiwgIm55b255byIpLCBwYXR0ZXJuID0gIm55IiwgcmVwbGFjZW1lbnQgPSAi8SIpDQpgYGANCmBgYHtyIHJlZ2V4OH0NCnN0cl9yZXBsYWNlX2FsbChjKCJjYXN0YW55YSIsICJvdG9ueW8iLCAidmVyb255byIsICJhbnlvIiwgIm55b255byIpLCBwYXR0ZXJuID0gIm55IiwgcmVwbGFjZW1lbnQgPSAi8SIpDQpgYGANCisgYHN0cl9zcGxpdChzdHJpbmcsIHBhdHRlcm4pYDogc2VwYXJhIHVuYSBjYWRlbmEgZW4gdW4gdmVjdG9yLCBgc3RyX3NwbGl0X2ZpeGVkKHN0cmluZywgcGF0dGVybiwgbilgIGxvIGhhY2UgZW4gdW4gbvptZXJvIGBuYCBkZXRlcm1pbmFkbyBkZSBlbGVtZW50b3MNCg0KYGBge3IgcmVnZXg5fQ0KcHJpbnQoc3RyX3NwbGl0KCJFcmVzIG11eSBjaHUgY2h1IGNodWxpIixwYXR0ZXJuID0gIiAiKSkNCnByaW50KGxlbmd0aChzdHJfc3BsaXQoIkVyZXMgbXV5IGNodSBjaHUgY2h1bGkiLHBhdHRlcm4gPSAiICIpW1sxXV0pKQ0KYGBgDQoNCiMjIEFuYWxpemFuZG8gdGV4dG8gY29uIFI6IGxvcyBTaW1wc29ucyBmdWVudGUgZGUgc2FiaWR1cu1hDQoNCiMjIyBMZWVyIGxvcyBzdWJ0aXR1dG9zDQoNCkxhIGxpYnJlcu1hIGBzdWJ0b29sc2AgcGVybWl0ZSBsZWVyIGFyY2hpdm9zIGAuc3RyYCB5ICBgLnN1YmAsIGFz7SBjb21vIG9yZ2FuaXphciBjYWRhIGRp4WxvZ28gZW4gdW4gZGF0YSBmcmFtZS4gDQpMb3MgYXJjaGl2b3MgaGFuIGRlIGVzdGFyIG9yZ2FuaXphZG9zIGVuIGRpcmVjdG9yaW9zIHBvciB0ZW1wb3JhZGFzIHkgY2FkYSB1bm8gaGEgZGUgZXN0YXIgbm9tYnJhZG8gY29tbyBgUzAxeEUwMWAgcGFyYSBxdWUgc2UgcGFyc2VlIGNvcnJlY3RhbWVudGUgZWwgbvptZXJvIGRlIHRlbXBvcmFkYSB5IGRlIGVwaXNvZGlvLg0KDQpOb3MgY2VudHJhcmVtb3MgZW4gbGFzIDkgcHJpbWVyYXMgdGVtcG9yYWRhcyBkZSBsb3MgU2ltcHNvbnMuIFNpIGRlc2NhcmdhbW9zIGxhIHB1bnR1YWNp824geSByZXByZXNlbnRhbW9zIGdy4WZpY2FtZW50ZSBlbCBwcm9tZWRpbyBkZWwgc2NvcmUgcG9yIHRlbXBvcmFkYSwgc2Ugb2JzZXJ2YSB1biBjbGFybyBkZXNjZW5zbyBhIHBhcnRpciBkZSBsYSAxMC4gDQpQb3Igb3RybyBsYWRvIHNvbiBsYXMgdGVtcG9yYWRhcyBxdWUgbWVqb3IgY29ub2NlbW9zIGdyYWNpYXMgYSBzdXMgbnVtZXJvc2FzIHJlcGV0aWNpb25lcyA6KQ0KDQoNCmBgYHtyLCBvdXQud2lkdGggPSAiMTUwcHgifQ0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoIkM6L1VzZXJzL0NsYXVkaWFHL0RvY3VtZW50cy9ybGFkaWVzL3JsYWRpZXNfcmVwby9ybGFkaWVzX3RleHRtaW5pbmcvaW1hZ2VzL3JhdGluZ3MucG5nIixkcGkgPSAxMDApDQoNCmBgYA0KDQoNCg0KQSAgY29udGludWFjafNuIGxlZW1vcyBsb3Mgc3VidO10dWxvcyAoIF9lbiBpbmds6XNfICkgZGUgbGFzIDkgcHJpbWVyYXMgdGVtcG9yYWRhcyBkZSBsb3MgU2ltcHNvbnMuIA0KDQoNCmBgYHtyIHN1YnRvb2xzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBwYWdlZC5wcmludD1GQUxTRX0NCmxpYnJhcnkoc3VidG9vbHMpDQphIDwtIHJlYWQuc3VidGl0bGVzLnNlcmllKGRpciA9ICJDOi9Vc2Vycy9DbGF1ZGlhRy9Eb2N1bWVudHMvcmxhZGllcy9ybGFkaWVzX3JlcG8vcmxhZGllc190ZXh0bWluaW5nL1RoZSBTaW1wc29ucy8iKQ0KZGYgPC0gc3ViRGF0YUZyYW1lKGEpDQpkZiA8LSBkZltjb21wbGV0ZS5jYXNlcyhkZiksIF0NCnN0cihkZikNCmBgYA0KDQojIyMgUHJpbWVyIGFu4Wxpc2lzOiB0bQ0KDQpVbmEgcHJpbWVyYSBvcGNp824gZXMgZW1wbGVhciBsYSBsaWJyZXLtYSBgdG1gIHBhcmEgbnVlc3RybyBhbuFsaXNpcy4gTGEgZnVuY2nzbiBgdG1fbWFwYCBub3MgdmEgYSBwZXJtaXRpciBwcmVwYXJhciBudWVzdG9yIGRvY3VtZW50byBwYXJhIGVsIGFu4Wxpc2lzOg0KDQorIGNvbmZvcm1hciB1biBjb3JwdXMgKGNvbmp1bnRvIGVzdHJ1Y3R1cmFkbyBkZSB0ZXh0b3MgLyBkb2N1bWVudG9zICkNCisgdHJhbnNmb3JtYXIgZW4gbWlu+nNjdWxhcyBudWVzdHJvIHRleHRvDQorIGVsaW1pbmFyIGxvcyBzaWdub3MgZGUgcHVudHVhY2nzbg0KKyBlbGltaW5hciBsb3MgbvptZXJvcw0KKyBlbGltaW5hciBsYXMgX3N0b3B3b3Jkc18gDQorIGVsaW1pbmFyIGxvcyBlc3BhY2lvcyBlbiBibGFuY28NCg0KYGBge3IgY2xlYW50bSwgZWNobz1UUlVFLCBtZXNzYWdlPVRSVUUsIHdhcm5pbmc9VFJVRX0NCmxpYnJhcnkodG0pDQpjIDwtIHRtQ29ycHVzKGEpDQpjIDwtIHRtX21hcChjLCBjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKQ0KYyA8LSB0bV9tYXAoYywgcmVtb3ZlUHVuY3R1YXRpb24pDQpjIDwtIHRtX21hcChjLCByZW1vdmVOdW1iZXJzKQ0KYyA8LSB0bV9tYXAoYywgcmVtb3ZlV29yZHMsIHN0b3B3b3JkcygiZW5nbGlzaCIpKQ0KYyA8LSB0bV9tYXAoYywgc3RyaXBXaGl0ZXNwYWNlKQ0KYw0KYGBgDQoNCkVsIHNlZ3VuZG8gcGFzbywgdW5hIHZleiBwcmVwYXJhZG8gbnVlc3RybyBjb3JwdXMsIHNlcuEgZWwgZGUgY29uc3RydWlyIGxhIG1hdHJpeiBkZSB06XJtaW5vcyBkb2N1bWVudG9zLiBQYXJhIHNpbXBsaWZpY2FyIGVsIGFu4Wxpc2lzIGNhZGEgdGVtcG9yYWRhIGNvbnN0aXR1aXLhIHVuIGRvY3VtZW50by4gDQpEZSBlc3RhIGZvcm1hIG9idGVuZW1vcyBwYXJhIGNhZGEgdOlybWlubyBsYSBmcmVjdWVuY2lhIHBvciB0ZW1wb3JhZGEuIA0KDQpgYGB7ciwgLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KVERNIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChjKQ0KVERNIDwtIGFzLm1hdHJpeChURE0pDQp2ZWMuc2Vhc29uIDwtIGMocmVwKHggPSAxLDEzKSwgcmVwKDIsIDIyKSwgcmVwKDMsMjQpLCByZXAoNCwyMiksIHJlcCg1LDI0KSwgcmVwKDYsMjUpLCByZXAoNywyNSkscmVwKDgsMjUpLCByZXAoOSwyNSkpICNlcGlzb2Rpb3MgcG9yIHRlbXANClRETS5zZWFzb24gPC0gdChhcHBseShURE0sIDEsIGZ1bmN0aW9uKHgpIHRhcHBseSh4LCB2ZWMuc2Vhc29uLCBzdW0pKSkNCmNvbG5hbWVzKFRETS5zZWFzb24pIDwtIHBhc3RlMCgiU18iLCB1bmlxdWUodmVjLnNlYXNvbikpDQpoZWFkKFRETS5zZWFzb24pDQpgYGANCg0KQSBjb250aW51YWNp824gcmVwcmVzZW50YW1vcyBlbiB1bmEgbnViZSBkZSB06XJtaW5vcyBkaWNoYXMgZnJlY3VlbmNpYXM6IGVsIHRhbWHxbyBpbmRpY2EgZWwgbvptZXJvIGRlIHJlcGV0aWNpb25lcyB5IGVsIGNvbG9yIGxhIHRlbXBvcmFkYSBlbiBsYSBxdWUgbeFzIHJlcGV0aWNpb25lcyBwcmVzZW50YS4gTGEgcG9zaWNp824gZGVsIHTpcm1pbm8gcmVzcGVjdG8gZGUgbGEgZXRpcXVldGEgZGUgdGVtcG9yYWRhIGluZGljYSB0YW1iaeluIGxhIGZyZWN1ZW5jaWEgcG9yIHRlbXBvcmFkYS4gDQoNCmBgYHtyLCAsIG1lc3NhZ2U9VFJVRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkod29yZGNsb3VkKQ0Kc2V0LnNlZWQoMTAwKQ0KY29tcGFyaXNvbi5jbG91ZChURE0uc2Vhc29uLCB0aXRsZS5zaXplID0gMSwgbWF4LndvcmRzID0gMjAwLCByYW5kb20ub3JkZXIgPSBUKQ0KDQpgYGANCg0KIyMjIFRleHQgbWluaW5nIGNvbiB0aWR5dGV4dA0KDQpMYSBsaWJyZXLtYSB0aWR5dGV4dCBub3MgcGVybWl0ZSByZWFsaXphciBvcGVyYWNpb25lcyBjb21vIHRmIChmcmVjdWVuY2lhIGRlIHTpcm1pbm9zKSBvIHRmX2lkZiAoZnJlY3VlbmNpYSBkZSB06XJtaW5vIC0gZnJlY3VlbmNpYSBpbnZlcnNhIGRlIGRvY3VtZW50bykgY29uIG1heW9yIGFnaWxpZGFkLiANCg0KRGUgbnVldm8gdmFtb3MgYSBwcmVwYXJhciBlbCBudWVzdHJvcyBkaeFsb2dvcyBwYXJhIGVsIGFu4Wxpc2lzIGVsaW1pbmFkbyBsYXMgc3RvcHdvcmRzLg0KDQpgYGB7ciBlbGltaW5hciBzdG9wd29yZHMsIGVjaG89VFJVRSwgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkodGlkeXRleHQpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KZGF0YShzdG9wX3dvcmRzKQ0KDQp0aWR5X2RmIDwtIGRmICU+JQ0KICB1bm5lc3RfdG9rZW5zKHdvcmQsIFRleHQpICU+JQ0KICBkcGx5cjo6YW50aV9qb2luKHN0b3Bfd29yZHMpDQoNCmBgYA0KDQpFbGltaW5hbW9zIGFkZW3hcyBhcXVlbGxhcyAicGFsYWJyYXMiIGNvbnN0aXR1aWRhcyBleGNsdXNpdmFtZW50ZSBwb3IgbvptZXJvcyB5IGxhIHBhbGFicmEgc2ltcHNvbi4NCg0KYGBge3Igd2FybmluZz1GQUxTRSwgLGVjaG89VFJVRX0NCmxpYnJhcnkoZGF0YS50YWJsZSkNCnRpZHlfZGYgPC0gYXMuZGF0YS50YWJsZSh0aWR5X2RmKQ0KdGlkeV9kZiA8LSB0aWR5X2RmW2lzLm5hKGFzLm51bWVyaWMod29yZCkpXQ0KdGlkeV9kZiA8LSB0aWR5X2RmW3dvcmQgIT0gJ3NpbXBzb24nXQ0KYGBgDQoNCg0KQ29uIG51ZXN0cm8gZGF0YSBzZXQgbGltcGlvIHBvZGVtb3MgcmVwcmVzZW50YXIgZW4gdW4gZ3LhZmljbyBkZSBiYXJyYXMgbGEgZnJlY3VlbmNpYSBkZSB06XJtaW5vcy4gTG8gcXVlIGhhcmVtb3Mgc2Vy4ToNCg0KKyBhZ3J1cGFyIHBvciB0ZW1wb3JhZGENCisgc3VtYXIgZWwgbvptZXJvIGRlIHZlY2VzIHF1ZSBhcGFyZWNlIGVsIHTpcm1pbm8gKGBjb3VudGApDQorIHRvbWFyIGVsIHRvcCAxMCBwb3IgdGVtcG9yYWRhDQorIHJlcHJlc2VudGFyIGVuIHVuIF9iYXJwbG90XyBwYXJhIGNhZGEgdGVtcG9yYWRhIGRpY2hhIGZyZWN1ZW5jaWENCg0KDQpgYGB7ciBwbG90LCBmaWcud2lkdGg9MTAsIG1lc3NhZ2U9VFJVRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZ2dwbG90MikNCnRpZHlfZGYgJT4lIGdyb3VwX2J5KHNlYXNvbikgJT4lDQogICAgICAgIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFKSAlPiUNCiAgICAgICAgdG9wX24oMTUpICU+JQ0KICAgICAgICBnZ3Bsb3QoYWVzKHJlb3JkZXIod29yZCxuKSwgbiwgZmlsbCA9IHNlYXNvbikpICsNCiAgICAgICAgZ2VvbV9jb2woKSArDQogICAgICAgIGNvb3JkX2ZsaXAoKSArDQogICAgICAgIGZhY2V0X3dyYXAofnNlYXNvbiwgc2NhbGVzID0gImZyZWVfeSIpICsNCiAgICAgICAgbGFicyh4ID0gTlVMTCkgKw0KICAgICAgICBndWlkZXMoZmlsbCA9IEZBTFNFKSArDQogICAgICAgIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGUgPSAiU2V0MSIpDQpgYGANCg0KQWhvcmEgcXVlIGRpc3BvbmVtb3MgZGUgbGEgZnJlY3VlbmNpYSBwb2RlbW9zIGFuYWxpemFyIGxhIGV2b2x1Y2nzbiBlbiBzdXMgYXBhcmljaW9uZXMgLyB0cmFtYXMgZGUgY2FkYSBwZXJzb25hamU6DQorIGNhbGN1bGFtb3MgbGEgZnJlY3VlbmNpYSBwYXJhIHRvZG9zIGxvcyB06XJtaW5vcywgbm8gc29sbyBlbCB0b3AgMTUNCisgY3JlYW1vcyBkb3MgbGlzdGFzLCBsYSBmYW1pbGlhIFNpbXBzb24geSBvdHJvcyBwZXJzb25hamVzIHJlbGV2YW50ZXMgZGUgbGEgdHJhbWENCisgcmVwcmVzZW50YW1vcyBhbWJhcyBzZXJpZXMgc2VyaWVzIHRlbXBvcmFsZXMgDQoNCmBgYHtyIGZpZy5oZWlnaHQ9OCwgZmlnLndpZHRoPTgsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpyZXF1aXJlKHBsb3RseSkNCnRpZHlfdGYgPC0gdGlkeV9kZiAlPiUgZ3JvdXBfYnkoc2Vhc29uKSAlPiUNCiAgICAgICAgY291bnQod29yZCwgc29ydCA9IFRSVUUpDQp0aWR5X3RmIDwtIGFzLmRhdGEudGFibGUodGlkeV90ZikNCg0Kc2ltcHNvbl9mYW1pbHkgPC0gYygnaG9tZXInLCAnYmFydCcsICdsaXNhJywgJ21hZ2dpZScsICdtYXJnZScsICdwYXR0eScsJ3NlbG1hJykNCm90aGVyX2NoYXJhY3RlcnMgPC0gIGMoJ21vZScsICduZWQnLCAnYmFybmV5JywgJ21vZGQnLCAnaXRjaHknLCAnc2NyYXRjaHknLCAna3J1c3R5JywgJ2J1cm5zJywgJ2xlbm55JywgJ2NhcmwnLCAnZWRuYScsICduZWxzb24nLCAnYXB1JywgJ21pbGhvdXNlJywgJ3JhbHBoJywgJ3NraW5uZXInLCAnYm9iJykNCg0KbXlwbG90IDwtIGdncGxvdCh0aWR5X3RmW3RpZHlfdGYkd29yZCAlaW4lIHNpbXBzb25fZmFtaWx5XSwgYWVzKHg9c2Vhc29uLCB5PW4sIGdyb3VwPXdvcmQpKSArDQogIGdlb21fbGluZShhZXMoY29sb3I9d29yZCksIHNpemU9MS4yNSkrDQogIGdlb21fcG9pbnQoYWVzKGNvbG9yPXdvcmQpKQ0KDQpnZ3Bsb3RseShteXBsb3QpDQoNCmBgYA0KDQpgYGB7ciBmaWcuaGVpZ2h0PTgsIGZpZy53aWR0aD04LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbXlwbG90IDwtIGdncGxvdCh0aWR5X3RmW3RpZHlfdGYkd29yZCAlaW4lIG90aGVyX2NoYXJhY3RlcnNdLCBhZXMoeD1zZWFzb24sIHk9biwgZ3JvdXA9d29yZCkpICsNCiAgZ2VvbV9saW5lKGFlcyhjb2xvcj13b3JkKSwgc2l6ZT0wLjc1KSsNCiAgZ2VvbV9wb2ludChhZXMoY29sb3I9d29yZCkpDQpnZ3Bsb3RseShteXBsb3QpDQpgYGANCg0KDQpUYW1iaeluIHBvZGVtb3Mgb2J0ZW5lciB5IHJlcHJlc2VudGFyIGxvcyBiaWdyYW1hcyAoY29uanVudG8gZGUgMiB06XJtaW5vcykgeSBzdXMgZnJlY3VlbmNpYXMuIFNlIHJlcHJlc2VudGFuIGVuIGZvcm1hIGRlIGdyYWZvIGxvcyBt4XMgY29tdW5lcyAoYXF1ZWxsb3MgcXVlIGFwYXJlY2VuIGFsIG1lbm9zIDcgdmVjZXMgZW4gdW5hIG1pc21hIHRlbXBvcmFkYSkNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHlyKQ0KbGlicmFyeShpZ3JhcGgpDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KDQpiaWdyYW1fZ3JhcGggPC0gZGYgJT4lDQogIHVubmVzdF90b2tlbnMoYmlncmFtLCBUZXh0LCB0b2tlbiA9ICJuZ3JhbXMiLCBuID0gMikgJT4lDQogIHNlcGFyYXRlKGJpZ3JhbSwgYygid29yZDEiLCAid29yZDIiKSwgc2VwID0gIiAiKSAlPiUNCiAgZmlsdGVyKCF3b3JkMSAlaW4lIHN0b3Bfd29yZHMkd29yZCkgJT4lDQogIGZpbHRlcighd29yZDIgJWluJSBzdG9wX3dvcmRzJHdvcmQpICU+JSANCiAgZ3JvdXBfYnkoc2Vhc29uKSAlPiUNCiAgY291bnQod29yZDEsIHdvcmQyLCBzb3J0ID0gVFJVRSkgJT4lDQogIHNlbGVjdCh3b3JkMSwgd29yZDIsIHNlYXNvbiwgbikgJT4lDQogIGZpbHRlcihuID4gNykgJT4lDQogIGdyYXBoX2Zyb21fZGF0YV9mcmFtZSgpDQojIHN0cihiaWdyYW1fZ3JhcGgpDQpgYGANCg0KYGBge3IgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwLCBtZXNzYWdlPVRSVUUsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KGdncmFwaCkNCnNldC5zZWVkKDIwMTcpDQoNCmdncmFwaChiaWdyYW1fZ3JhcGgsIGxheW91dCA9ICJmciIpICsNCiAgZ2VvbV9lZGdlX2xpbmsoKSArDQogIGdlb21fbm9kZV9wb2ludCgpICsNCiAgZ2VvbV9ub2RlX3RleHQoYWVzKGxhYmVsID0gbmFtZSksIHZqdXN0ID0gMSwgaGp1c3QgPSAxKQ0KYGBgDQpgYGB7cn0NCmEgPC0gZ3JpZDo6YXJyb3codHlwZSA9ICJjbG9zZWQiLCBsZW5ndGggPSB1bml0KC4xNSwgImluY2hlcyIpKQ0KDQpnZ3JhcGgoYmlncmFtX2dyYXBoLCBsYXlvdXQgPSAiZnIiKSArDQogIGdlb21fZWRnZV9saW5rKGFlcyhlZGdlX2FscGhhID0gbiksIHNob3cubGVnZW5kID0gRkFMU0UsDQogICAgICAgICAgICAgICAgIGFycm93ID0gYSwgZW5kX2NhcCA9IGNpcmNsZSguMDcsICdpbmNoZXMnKSkgKw0KICBnZW9tX25vZGVfcG9pbnQoY29sb3IgPSAibGlnaHRibHVlIiwgc2l6ZSA9IDUpICsNCiAgZ2VvbV9ub2RlX3RleHQoYWVzKGxhYmVsID0gbmFtZSksIHZqdXN0ID0gMSwgaGp1c3QgPSAxKSArDQogIHRoZW1lX3ZvaWQoKQ0KYGBgDQoNClRhbWJp6W4gcG9kZW1vcyByZWFsaXphciB0Zl9pZGYgc29icmUgbnVlc3RybyBjb3JwdXMgcGFyYSBsb2NhbGl6YXIgbGFzIHBhbGFicmFzIG3hcyByZWxldmFudGVzLCBlbiBnZW5lcmFsIHkgcG9yIHRlbXBvcmFkYQ0KDQpgYGB7cn0NCmxpYnJhcnkoZHBseXIpDQp0Zl9pZGZfZGYgPC0gdGlkeV9kZiAlPiUgDQogICAgICAgIGNvdW50KHNlYXNvbiwgd29yZCwgc29ydCA9IFRSVUUpICU+JQ0KICAgICAgICBiaW5kX3RmX2lkZih3b3JkLCBzZWFzb24sIG4pDQp0Zl9pZGZfZGYgPC0gdGZfaWRmX2RmW29yZGVyKC10Zl9pZGZfZGYkdGZfaWRmKSxdIA0KYGBgDQoNCmBgYHtyfQ0KdGZfaWRmX2RmICU+JSANCiAgdG9wX24oMjApICU+JQ0KICBnZ3Bsb3QoYWVzKHdvcmQsIHRmX2lkZiwgZmlsbCA9IHNlYXNvbikpICsNCiAgZ2VvbV9jb2woKSArDQogIGxhYnMoeCA9IE5VTEwsIHkgPSAidGYtaWRmIikgKw0KICBjb29yZF9mbGlwKCkNCmBgYA0KYGBge3IgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9OCx9DQp0Zl9pZGZfZGYgJT4lIA0KICBncm91cF9ieShzZWFzb24pICU+JSANCiAgdG9wX24oOCkgJT4lIA0KICB1bmdyb3VwICU+JQ0KICBnZ3Bsb3QoYWVzKHJlb3JkZXIod29yZCwgdGZfaWRmKSwgdGZfaWRmLCBmaWxsID0gc2Vhc29uKSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIGxhYnMoeCA9IE5VTEwsIHkgPSAidGYtaWRmIikgKw0KICBmYWNldF93cmFwKH5zZWFzb24sIG5jb2wgPSAyLCBzY2FsZXMgPSAiZnJlZSIpICsNCiAgY29vcmRfZmxpcCgpDQpgYGANCg0KDQoNCiMjIEFuZXhvcyB5IG90cmEgaW5mb3Jt4WNp824g+nRpbA0KKyBTdWJ0b29sczogaHR0cDovL3d3dy5waWVjZW9may5mci8/cD00MzcNCisgU3RyaW5nciBhbmQgUmVnZXg6IGh0dHBzOi8vcnN0dWRpby1wdWJzLXN0YXRpYy5zMy5hbWF6b25hd3MuY29tLzE4MDYxMF9kMzc2NGM0M2YxZTU0NjkyYjdlODRkMjFlYzk0NzcyYS5odG1sDQorIEFuYWxpc2lzIFJpY2smTW9ydHk6IGh0dHA6Ly90YW1hc3ppbGFneWkuY29tL2Jsb2cvYS10aWR5LXRleHQtYW5hbHlzaXMtb2Ytcmljay1hbmQtbW9ydHkvDQorIEdncGxvdCBjaGVhdHNoZWV0OiBodHRwczovL3d3dy5yc3R1ZGlvLmNvbS93cC1jb250ZW50L3VwbG9hZHMvMjAxNS8wMy9nZ3Bsb3QyLWNoZWF0c2hlZXQucGRmDQorIFBsb3RseSBjaGVhdHNoZWV0OiBodHRwczovL2ltYWdlcy5wbG90Lmx5L3Bsb3RseS1kb2N1bWVudGF0aW9uL2ltYWdlcy9yX2NoZWF0X3NoZWV0LnBkZg0K